﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Castle.DynamicProxy.Generators.Emitters.SimpleAST;
using System.Linq.Expressions;
using Castle.Core.Interceptor;
using Legend.Fakes.Configuration;
using Legend.Fakes.Tests;
using Legend.Fakes.Api;
using Legend.Fakes.Assertion;

namespace Legend.Fakes.Tests.Api
{
    [TestFixture]
    public class FakeTests
    {
        private static readonly IFakeObjectCallRule NonApplicableInterception = new FakeCallRule { IsApplicableTo = x => false };

        [Test]
        public void Single_parameter_constructor_throws_when_type_is_null()
        {
            Assert.Throws<ArgumentNullException>(() =>
                new FakeObject((Type)null));
        }

        [Test]
        public void Two_parameters_constructor_throws_when_type_is_null()
        {
            Assert.Throws<ArgumentNullException>(() =>
                new FakeObject((Type)null, new object[] { }));
        }

        [Test]
        public void Constructor_throws_when_argumentsForConstructor_is_null()
        {
            Assert.Throws<ArgumentNullException>(() =>
                new FakeObject(typeof(string), (object[])null));
        }

        [Test]
        public void Method_call_should_return_null_when_theres_no_matching_interception_and_return_type_is_reference_type()
        {
            var fake = new FakeObject(typeof(IFoo));
            var result = ((IFoo)fake.Object).Biz();
            
            Assert.That(result, Is.Null);
        }

        [Test]
        public void Method_call_should_return_default_value_when_theres_no_matching_interception_and_return_type_is_value_type()
        {
            var fake = new FakeObject(typeof(IFoo));
            var result = ((IFoo)fake.Object).Baz();

            Assert.That(result, Is.EqualTo(0));
        }

        [Test]
        public void Method_call_should_not_set_return_value_when_theres_no_matching_interception_and_return_type_is_void()
        {
            var fake = new FakeObject(typeof(IFoo));
            ((IFoo)fake.Object).Bar();
        }

        [Test]
        public void The_first_interceptor_should_be_applied_when_it_has_not_been_used()
        {
            var fake = new FakeObject(typeof(IFoo));

            var interception = new FakeCallRule
                {
                    IsApplicableTo = x => true
                };

            fake.AddRule(interception);

            // Act
            ((IFoo)fake.Object).Bar();
            
            Assert.That(interception.ApplyWasCalled, Is.True);
        }

        [Test]
        public void The_first_applicable_interceptor_should_be_called_when_it_has_not_been_used()
        {
            var fake = new FakeObject(typeof(IFoo));

            var interception = new FakeCallRule
            {
                IsApplicableTo = x => true
            };

            fake.AddRule(NonApplicableInterception);
            fake.AddRule(interception);

            ((IFoo)fake.Object).Bar();

            Assert.That(interception.ApplyWasCalled, Is.True);
        }

        [Test]
        public void The_second_applicable_action_should_be_called_on_the_second_call_when_the_first_applicable_action_has_no_numberOfTimes_specified()
        {
            var fake = new FakeObject(typeof(IFoo));

            var first = CreateApplicableInterception();
            var second = CreateApplicableInterception();

            fake.AddRule(first);
            fake.AddRule(second);

            ((IFoo)fake.Object).Bar();
            Assert.That(second.ApplyWasCalled, Is.False);

            ((IFoo)fake.Object).Bar();
            Assert.That(second.ApplyWasCalled, Is.True);
        }

        [Test]
        public void An_applicable_action_should_be_called_its_specified_number_of_times_before_the_next_applicable_action_is_called()
        {
            var fake = new FakeObject(typeof(IFoo));

            var applicableTwice = new FakeCallRule
                {
                    IsApplicableTo = x => true,
                    NumberOfTimesToCall = 2
                };

            var nextApplicable = CreateApplicableInterception();

            fake.AddRule(applicableTwice);
            fake.AddRule(nextApplicable);

            ((IFoo)fake.Object).Bar();
            ((IFoo)fake.Object).Bar();
            Assert.That(nextApplicable.ApplyWasCalled, Is.False);
            
            ((IFoo)fake.Object).Bar();
            Assert.That(nextApplicable.ApplyWasCalled, Is.True);
        }

        [Test]
        public void DefaultValue_should_be_returned_when_the_last_applicable_action_has_been_used_its_specified_number_of_times()
        {
            var fake = new FakeObject(typeof(IFoo));

            var applicableTwice = CreateApplicableInterception();
            applicableTwice.NumberOfTimesToCall = 2;
            applicableTwice.Apply = x => x.SetReturnValue(10);

            fake.AddRule(applicableTwice);

            ((IFoo)fake.Object).Baz();
            ((IFoo)fake.Object).Baz();

            var result = ((IFoo)fake.Object).Baz();
            
            Assert.That(result, Is.EqualTo(0));
        }

        [Test]
        public void Interceptions_should_return_interceptions_that_are_added()
        {
            var fake = new FakeObject(typeof(IFoo));

            var one = CreateApplicableInterception();
            var two = CreateApplicableInterception();

            fake.AddRule(one);
            fake.AddRule(two);

            Assert.That(fake.Rules, Is.EquivalentTo(new[] { one, two }));
        }

        [Test]
        public void Configure_should_return_fake_configuration_object()
        {
            var fake = A.Fake<IFoo>();
            
            var configuration = (FakeConfiguration<IFoo>)Fake.Configure(fake);
            
            Assert.That(configuration.Fake.Object, Is.EqualTo(fake));
        }

        [Test]
        public void Configure_should_throw_when_object_is_not_a_fake_object()
        {
            Assert.Throws<ArgumentException>(() =>
                Fake.Configure("non fake object"));
        }

        [Test]
        public void Verify_should_throw_when_call_has_been_made_thats_set_up_to_not_be_made()
        {
            var fake = A.Fake<IFoo>();

            AddFakeRule(fake, rule => 
            { 
                rule.IsApplicableTo = call => call.Method.Name.Equals("Bar");
                rule.NumberOfTimesToCall = 0;
                rule.MayNotBeCalledMoreThanTheNumberOfTimesSpecified = true;
            });
            
            fake.Bar();

            Assert.Throws<ExpectationException>(() =>
                Fake.Assert(fake).ExpectationsWereMet());
        }

        [Test]
        public void RecordedCalls_contains_all_calls_made_on_the_fake()
        {
            var fake = new FakeObject(typeof(IFoo));

            ((IFoo)fake.Object).Bar();
            var i = ((IFoo)fake.Object)[1];

            Assert.That(fake.RecordedCalls, Has.Some.Matches<IWriteableFakeObjectCall>(x => x.Method.Name == "Bar"));
            Assert.That(fake.RecordedCalls, Has.Some.Matches<IWriteableFakeObjectCall>(x => x.Method.Name == "get_Item"));
        }

        private static void AddFakeRule<T>(T fakedObject, Action<FakeCallRule> ruleConfiguration) where T : class
        {
            var rule = new FakeCallRule();
            ruleConfiguration(rule);

            AddFakeRule(fakedObject, rule);
        }

        private static void AddFakeRule<T>(T fakedObject, FakeCallRule rule) where T : class
        {
            Fake.GetFakeObject(fakedObject).AddRule(rule);
        }


        [Test]
        public void Verify_should_throw_when_expected_call_has_not_been_called_specified_number_of_times()
        {
            var fake = A.Fake<IFoo>();

            AddFakeRule(fake, new FakeCallRule
            {
                IsApplicableTo = x => x.Method.Name == "Bar",
                MustGetCalled = true,
                NumberOfTimesToCall = 2
            });

            fake.Bar();
            fake.Baz();

            Assert.Throws<ExpectationException>(() => Fake.GetFakeObject(fake).Verify());
        }

        [Test]
        public void Verify_should_not_throw_when_expected_call_has_been_called_specified_number_of_times()
        {
            var fake = A.Fake<IFoo>();

            AddFakeRule(fake, new FakeCallRule
            {
                IsApplicableTo = x => x.Method.Name == "Bar",
                MustGetCalled = true,
                NumberOfTimesToCall = 2
            });
            
            fake.Bar();
            fake.Bar();

            Fake.GetFakeObject(fake).Verify();
        }

        [Test]
        public void Verify_should_throw_when_MustHappen_is_set_and_no_matching_call_has_been_made()
        {
            var fake = A.Fake<IFoo>();

            AddFakeRule(fake, new FakeCallRule
            {
                IsApplicableTo = x => x.Method.Name == "Bar",
                MustGetCalled = true,
                NumberOfTimesToCall = 1
            });

            Assert.Throws<ExpectationException>(() =>
                Fake.GetFakeObject(fake).Verify());
        }

      
        [Test]
        public void Assert_returns_fake_asserter_with_faked_object_set_when_called_with_faked_object()
        {
            var foo = A.Fake<IFoo>();

            var asserter = (FakeAsserter<IFoo>)Fake.Assert(foo);

            Assert.That(asserter.Fake.Object, Is.EqualTo(foo));
        }

        [Test]
        public void Object_properties_has_property_behavior_when_not_configured()
        {
            var foo = A.Fake<IFoo>();

            foo.SomeProperty = 10;

            Assert.That(foo.SomeProperty, Is.EqualTo(10));
        }

        [Test]
        public void Object_properties_will_lose_property_behavior_when_configured()
        {
            var foo = A.Fake<IFoo>();

            foo.SomeProperty = 10;
            Fake.Configure(foo).CallsTo(x => x.SomeProperty).Returns(20);
            foo.SomeProperty = 10;

            Assert.That(foo.SomeProperty, Is.EqualTo(20));
        }

        [Test]
        public void Properties_should_be_set_to_fake_objects_when_property_type_is_fakeable_and_the_property_is_not_explicitly_set()
        {
            var foo = A.Fake<IFoo>();

            Fake.Configure(foo.ChildFoo).CallsTo(x => x.Baz()).Returns(10);

            Assert.That(foo.ChildFoo.Baz(), Is.EqualTo(10));
        }

        private static FakeCallRule CreateApplicableInterception()
        {
            return new FakeCallRule
            {
                IsApplicableTo = x => true
            };
        }
    }
}